import type { AxiosRequestConfig, AxiosInstance, AxiosResponse, AxiosError } from 'axios';
import type { RequestOptions, Result, UploadFileParams } from '/#/axios';
import type { CreateAxiosOptions } from './axiosTransform';
import axios from 'axios';
import qs from 'qs';
import { AxiosCanceler } from './axiosCancel';
import { isFunction } from '/@/utils/is';
import { cloneDeep } from 'lodash-es';
import { ContentTypeEnum } from '/@/enums/httpEnum';
import { RequestEnum } from '/@/enums/httpEnum';

export * from './axiosTransform';

// 判断是否需要过滤重复请求
function isIgnoreCancel(config: CreateAxiosOptions, options: CreateAxiosOptions) {
  const ignoreCancelToken = config.headers?.ignoreCancelToken;
  return ignoreCancelToken !== undefined
    ? ignoreCancelToken
    : options.requestOptions?.ignoreCancelToken ?? true;
}

// 对axios二次封装的类
export class VAxios {
  // axios实例
  private axiosInstance: AxiosInstance;
  // axios.create()时需要的配置参数
  private readonly options: CreateAxiosOptions;

  constructor(options: CreateAxiosOptions) {
    this.options = options;
    this.axiosInstance = axios.create(options);
    // 在实例化VAxios时就会对axios实例添加拦截器
    this.setupInterceptors();
  }

  // 创建axios实例
  private createAxios(config: CreateAxiosOptions): void {
    this.axiosInstance = axios.create(config);
  }

  // 获取传递过来的拦截器函数
  private getTransform() {
    const { transform } = this.options;
    return transform;
  }

  getAxios(): AxiosInstance {
    return this.axiosInstance;
  }

  // 重新配置axios（暂无地方使用）
  configAxios(config: CreateAxiosOptions) {
    if (!this.axiosInstance) {
      return;
    }
    this.createAxios(config);
  }

  // 设置请求头
  setHeader(headers: any): void {
    if (!this.axiosInstance) {
      return;
    }
    Object.assign(this.axiosInstance.defaults.headers, headers);
  }

  // 添加各个拦截器
  private setupInterceptors() {
    // 实例化VAxios时会把对应的配置项传递过来，transform便是其中一个属性
    const transform = this.getTransform();
    if (!transform) {
      return;
    }
    const {
      // 请求拦截器
      requestInterceptors,
      // 请求拦截器错误时的回调
      requestInterceptorsCatch,
      // 响应拦截器
      responseInterceptors,
      // 响应拦截器的错误处理
      responseInterceptorsCatch,
    } = transform;

    // 实例化取消请求自定义类
    const axiosCanceler = new AxiosCanceler();

    // 配置请求拦截器
    this.axiosInstance.interceptors.request.use((config: AxiosRequestConfig) => {
      // 如果ignoreCancelToken存在，则禁止取消重复请求（取消重复请求的功能将关闭）
      !isIgnoreCancel(config, this.options) && axiosCanceler.addPending(config);

      if (requestInterceptors && isFunction(requestInterceptors)) {
        config = requestInterceptors(config, this.options);
      }
      return config;
    }, undefined);

    // 捕获请求拦截器的错误
    requestInterceptorsCatch &&
      isFunction(requestInterceptorsCatch) &&
      this.axiosInstance.interceptors.request.use(undefined, requestInterceptorsCatch);

    // 处理响应拦截器
    this.axiosInstance.interceptors.response.use((res: AxiosResponse) => {
      !isIgnoreCancel(res.config, this.options) && axiosCanceler.removePending(res.config);
      if (responseInterceptors && isFunction(responseInterceptors)) {
        res = responseInterceptors(res);
      }
      return res;
    }, undefined);

    // 捕获响应拦截器的错误
    responseInterceptorsCatch &&
      isFunction(responseInterceptorsCatch) &&
      this.axiosInstance.interceptors.response.use(undefined, responseInterceptorsCatch);
  }

  // 文件上传 此方法仅适用于单个文件请求
  uploadFile<T = any>(config: AxiosRequestConfig, params: UploadFileParams) {
    const formData = new window.FormData();
    const customFilename = params.name || 'file';

    if (params.filename) {
      formData.append(customFilename, params.file, params.filename);
    } else {
      formData.append(customFilename, params.file);
    }

    // 如果还携带了额外参数 也会添加进formData里
    if (params.data) {
      Object.keys(params.data).forEach((key) => {
        const value = params.data![key];
        if (Array.isArray(value)) {
          value.forEach((item) => {
            formData.append(`${key}[]`, item);
          });
          return;
        }

        formData.append(key, params.data![key]);
      });
    }

    return this.axiosInstance.request<T>({
      ...config,
      method: 'POST',
      data: formData,
      headers: {
        'Content-type': ContentTypeEnum.FORM_DATA,
        // @ts-ignore
        ignoreCancelToken: true,
      },
    });
  }

  /**
   * 转序列化的操作：支持formData form-data qs application/x-www-form-urlencoded
   * 当 contentType === application/x-www-form-urlencoded时，且
   * 传递了data属性值，且
   * 请求方式不是get请求，此时会对data进行序列化操作
   */
  supportFormData(config: AxiosRequestConfig) {
    const headers = config.headers || this.options.headers;
    const contentType = headers?.['Content-Type'] || headers?.['content-type'];

    if (
      contentType !== ContentTypeEnum.FORM_URLENCODED ||
      !Reflect.has(config, 'data') ||
      config.method?.toUpperCase() === RequestEnum.GET
    ) {
      return config;
    }

    /**
     * qs示例
     * Qs.stringify(
     *  { a: ['b', 'c'] },
     *  {
     *    arrayFormat: 'brackets', // 指定数组转成的格式
     *    encode: false // 不进行编码 默认会进行编码
     *  }
     * )
     *
     * eval: a[]=b&a[]=c
     */
    return {
      ...config,
      data: qs.stringify(config.data, { arrayFormat: 'brackets' }),
    };
  }

  get<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
    return this.request({ ...config, method: 'GET' }, options);
  }

  post<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
    return this.request({ ...config, method: 'POST' }, options);
  }

  put<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
    return this.request({ ...config, method: 'PUT' }, options);
  }

  delete<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
    return this.request({ ...config, method: 'DELETE' }, options);
  }

  request<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
    // conf时定义请求时传递过来的一些配置参数 conf = config = AxiosRequestConfig
    let conf: CreateAxiosOptions = cloneDeep(config);
    const transform = this.getTransform();
    const { requestOptions } = this.options;

    // 合并实例化VAxios默认传递的options.requestOptions和调用接口时传递的RequestOptions配置项
    const opt: RequestOptions = Object.assign({}, requestOptions, options);

    const { beforeRequestHook, requestCatchHook, transformRequestHook } = transform || {};

    // 请求之前执行的一些操作（根据传递的配置处理传递过来的请求参数）
    if (beforeRequestHook && isFunction(beforeRequestHook)) {
      conf = beforeRequestHook(conf, opt);
    }
    conf.requestOptions = opt;

    // 对data转序列化操作（根据contentType进行相关的处理）
    conf = this.supportFormData(conf);

    return new Promise((resolve, reject) => {
      this.axiosInstance
        .request<any, AxiosResponse<Result>>(conf)
        .then((res: AxiosResponse<Result>) => {
          // transformRequestHook 处理请求过来的数据
          if (transformRequestHook && isFunction(transformRequestHook)) {
            try {
              const ret = transformRequestHook(res, opt);
              resolve(ret);
            } catch (err) {
              reject(err || new Error('request error!'));
            }
            return;
          }
          resolve(res as unknown as Promise<T>);
        })
        .catch((e: Error | AxiosError) => {
          if (requestCatchHook && isFunction(requestCatchHook)) {
            reject(requestCatchHook(e, opt));
            return;
          }
          if (axios.isAxiosError(e)) {
            // rewrite error message from axios in here
          }
          reject(e);
        });
    });
  }
}

/**
 * @author 徐轲
 * @time 2021-11-10 23:48:20
 * 关于封装的axios中各个模块的作用以及执行的顺序点：
 * 1. 定义VAxios类，类上挂载了很多与axios类似的请求方法
 * 2.实例化VAxios，传入配置参数
 * 3.实例化VAxios，添加拦截器
 *   3.1 添加请求拦截器的成功处理、添加请求拦截器的异常处理、添加响应拦截器的正常处理、添加响应拦截器的错误处理
 *   3.2 由于拦截器是先添加的后执行，所以先执行请求拦截器的错误拦截器对应的方法
 *       - 此处并没有针对捕获请求拦截器的错误做处理 如果需要可以添加
 *
 *  --- 以上是定义VAxios类与实例化VAxios类时做的一些操作 ---
 *
 *  --- 外部调用定义好的请求 ---
 *  1. 调用请求方法，根据调用的请求方法执行对应VAxios实例上的方法
 *  2. 根据调用的方法，最终都会执行实例上的request方法
 *  3. request方法执行
 *     3.1 执行beforeRequestHook方法，根据传递的配置处理传递过来的请求参数（主要根据请求方式处理不同的请求参数）
 *         - 判断是否需要拼接url前缀
 *         - 将基本请求地址与每个请求方法的请求地址进行拼接
 *         - 将data中可能存在的moment对象转成字符串，对所有字符串执行trim操作
 *         - 如果是get请求 判断是否需要添加时间戳到请求参数上，并且拼接到url上，兼容restful风格
 *         - 不是get请求，处理将data中包含moment类型的对象处理成时间字符串，处理value的空格问题
 *     3.2 根据条件判断，对data是否执行序列化字符串操作（qs.stringify）
 *     3.3 创建一个promise，代码体内执行axios.request方法，VAxios实例上的request方法的返回值就是当前定义的promise
 *     3.4 调用axios.request方法
 *         3.4.1 axios对应的请求拦截器执行
 *               - 处理是否需要过滤掉重复的请求（如果配置了ignoreCancelToken则忽略重复的请求不处理，反之将重复发送的请求取消掉）
 *               - 判断是否需要携带token，如果需要携带，先获取再设置
 *               - axios根据是否请求成功来执行对应的拦截器
 *         3.4.2 请求成功执行响应成功的拦截器（--------- 请求成功执行的操作 ------------）
 *                - 处理是否需要过滤请求，如果需要则删除掉请求拦截器中存储的请求
 *                - 执行responseInterceptors，响应拦截器内部的调用函数（此处无具体处理逻辑，直接返回了结果，所以直接进入了axios.request的then方法里）
 *         3.4.3 请求成功执行axios.request对应的then方法
 *         3.4.4 then方法执行
 *         3.4.5 then方法内部的transformRequestHook方法调用
 *               - transformRequestHook调用，处理：
 *               - 根据isReturnNativeResponse判断是否需要处理axios返回结果，如果需要直接返回res
 *               - 根据isTransformResponse判断是否需要处理axios返回结果，如果需要返回res.data
 *               - 如果没有返回结果 res.data = null 则抛出错误
 *               - 取到data中的各个参数，根据自己实际项目中自定义的自定义字段判断当前请求是否属于成功的请求，如果条件成立则直接返回后台返回的结果体 比如：res.data.data
 *               - 如果上面的条件不成立，也就是属于当前请求时失败的请求
 *                 - 根据后台返回的code值判断该执行什么样的业务逻辑，比如token过期，则处理token过期的问题
 *                 - 判断配置项中是否传递了errorMessageMode，用于错误提示时使用哪种方式（也可以不提示）
 *                 - 最终抛出异常，异常信息为优先使用后台返回的错误信息，不存在则使用默认提示，此时request.then内部通过捕获异常的方式捕获到，调用外部promise回调中的reject方法，VAxios实例上的request方法执行完毕，异常需要调用api的使用者去捕获（此处抛出的异常为message信息，可以自定义）
 *
 *         3.4.6 请求失败执行响应拦截器中的失败拦截器（--------- 请求失败执行的操作 ------------）
 *               - 注意：此处处理的仅仅是axios发送请求时失败了，不会因为响应成功的拦截器抛出了异常被这个地方所捕获，因为成功的拦截器先定义，先定义的拦截器后执行，如果请求成功的拦截器中抛出了异常则会执行this.axiosInstance.request中的catch方法
 *               - 执行对应的错误方法 responseInterceptorsCatch方法
 *                 - 执行收集ajax错误信息的功能
 *                 - 判断error对象，根据error对象中的信息设置errMessage提示
 *                 - 如果errMessage是系统层面的错误，比如：接口超时，跨域等，根据errorMessageMode对应的值弹出提示（值为'none'不进行任何弹出提示）返回一个失败的请求，参数是当前error对象
 *                 - 如果不是以上的错误，则调用checkStatus方法，根据浏览器状态码进行校验错误
 *                 - 最终返回一个失败的promise
 *         3.4.7 catch方法执行（this.axiosInstance().catch）
 *               - 判断是否定义了requestCatchHook，即当前catch中的hook函数，存在即执行
 *               - 判断是否是axios中抛出的error对象，如果是执行对应代码
 *               - 最终调用外部promise回调中的reject方法
 */
